revamp user profile page to new ui (#12507)

* revamp user profile page to new ui

* sonar fixes

* fixbuild failure

* Rename Task-ic.svg to task-ic.svg

* changes as per comments
This commit is contained in:
Ashish Gupta 2023-07-20 17:52:00 +05:30 committed by GitHub
parent ab1ec50c2c
commit 225c3667df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 1178 additions and 799 deletions

View File

@ -165,14 +165,13 @@ describe('DataConsumer Edit policy should work properly', () => {
.should('be.visible')
.click({ force: true });
verifyResponseStatusCode('@getUserPage', 200);
cy.get('[data-testid="left-panel"]').should(
'contain',
`${CREDENTIALS.firstName}${CREDENTIALS.lastName}`
);
cy.get(
'[data-testid="user-profile"] [data-testid="user-profile-details"]'
).should('contain', `${CREDENTIALS.firstName}${CREDENTIALS.lastName}`);
cy.get('[data-testid="left-panel"]')
.should('be.visible')
.should('contain', policy);
cy.get(
'[data-testid="user-profile"] [data-testid="user-profile-inherited-roles"]'
).should('contain', policy);
});
it('Check if the new user has only edit access on description and tags', () => {

View File

@ -139,8 +139,8 @@ describe('Test Add role and assign it to the user', () => {
cy.get(`[data-testid="${userName}"]`).should('be.visible').click();
verifyResponseStatusCode('@userDetailsPage', 200);
cy.get('[data-testid="left-panel"]')
.should('be.visible')
.should('contain', roleName);
cy.get(
'[data-testid="user-profile"] [data-testid="user-profile-roles"]'
).should('contain', roleName);
});
});

View File

@ -82,10 +82,9 @@ describe('Login flow should work properly', () => {
.should('be.visible')
.click({ force: true });
verifyResponseStatusCode('@getUser', 200);
cy.get('[data-testid="left-panel"]').should(
'contain',
`${CREDENTIALS.firstName}${CREDENTIALS.lastName}`
);
cy.get(
'[data-testid="user-profile"] [data-testid="user-profile-details"]'
).should('contain', `${CREDENTIALS.firstName}${CREDENTIALS.lastName}`);
});
it('Signin using invalid credentials', () => {

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" fill="none"><path fill="#0968da" stroke="#0968da" stroke-width=".2" d="M11.278 2.166H9.955v-.592c0-.191-.21-.278-.4-.278H8.526C8.283.599 7.674.25 6.977.25a1.584 1.584 0 0 0-1.549 1.045h-1.01c-.19 0-.382.087-.382.278v.592H2.712a1.48 1.48 0 0 0-1.462 1.41v8.85c0 .767.696 1.324 1.462 1.324h8.566c.766 0 1.462-.557 1.462-1.323v-8.85a1.48 1.48 0 0 0-1.462-1.41Zm-6.546-.174h.957a.383.383 0 0 0 .331-.313 1.01 1.01 0 0 1 .958-.784.992.992 0 0 1 .94.784c.031.171.174.3.348.313h.992v1.393H4.732V1.992Zm7.312 10.435c0 .383-.383.627-.766.627H2.712c-.383 0-.766-.244-.766-.627v-8.85a.783.783 0 0 1 .766-.714h1.324v.887a.366.366 0 0 0 .383.331h5.135c.2.011.374-.133.4-.33v-.888h1.324c.4.007.73.315.766.713v8.85Z"/><path fill="#0968da" stroke="#0968da" stroke-width=".2" d="M5.663 9.525a.283.283 0 0 0-.396-.014l-.906.863-.382-.396a.283.283 0 0 0-.397-.014.297.297 0 0 0 0 .41l.58.595c.05.056.123.087.199.085a.283.283 0 0 0 .198-.085L5.663 9.92a.269.269 0 0 0 0-.396ZM10.243 10H7.368c-.138 0-.25.167-.25.374s.112.375.25.375h2.875c.138 0 .25-.168.25-.375S10.38 10 10.243 10ZM5.663 6.526a.283.283 0 0 0-.396-.014l-.906.863-.382-.396a.283.283 0 0 0-.397-.014.297.297 0 0 0 0 .41l.58.594c.05.057.123.088.199.085a.283.283 0 0 0 .198-.085l1.104-1.047a.269.269 0 0 0 0-.396ZM10.243 7H7.368c-.138 0-.25.168-.25.375s.112.375.25.375h2.875c.138 0 .25-.168.25-.375S10.38 7 10.243 7Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,8 @@
<svg viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.50781 1.66259L1.28027 2.47978L1.07812 2.06972C0.966797 1.84126 0.84082 1.63037 0.796875 1.5923C0.685547 1.49857 0.527344 1.45171 0.398438 1.47514C0.146484 1.522 0 1.69774 0 1.94378C0 2.07265 0.0439453 2.17809 0.366211 2.80782C0.782227 3.63087 0.805664 3.65723 1.09863 3.65723H1.27148L2.76562 2.66137C3.84375 1.94378 4.27734 1.63916 4.31836 1.56887C4.39746 1.43999 4.39746 1.1881 4.31836 1.0563C4.23047 0.912776 4.09863 0.845409 3.90234 0.845409H3.73535L2.50781 1.66259Z" fill="currentColor"/>
<path d="M5.99414 1.37228C5.85352 1.46015 5.7832 1.59196 5.7832 1.78234C5.7832 1.97272 5.85352 2.10453 5.99414 2.1924C6.08789 2.25098 6.14941 2.25098 9.5332 2.25098C12.917 2.25098 12.9785 2.25098 13.0723 2.1924C13.2129 2.10453 13.2832 1.97272 13.2832 1.78234C13.2832 1.59196 13.2129 1.46015 13.0723 1.37228C12.9785 1.3137 12.917 1.3137 9.5332 1.3137C6.14941 1.3137 6.08789 1.3137 5.99414 1.37228Z" fill="black" fill-opacity="0.4"/>
<path d="M3.82031 4.59443C3.75293 4.60615 3.29883 4.89319 2.52539 5.40869C1.86914 5.84803 1.32129 6.20537 1.30957 6.20537C1.29492 6.20537 1.18945 6.01499 1.07227 5.7836C0.837891 5.31789 0.776367 5.25345 0.536133 5.22123C0.436523 5.20952 0.37793 5.22123 0.275391 5.27396C0.0966797 5.36475 0.0117188 5.49949 0.0117188 5.68987C0.0117188 5.82167 0.0527344 5.91833 0.37207 6.55099C0.779297 7.36524 0.814453 7.40625 1.08984 7.40625C1.1748 7.40625 1.28613 7.38575 1.33887 7.35939C1.56152 7.24223 4.22754 5.44091 4.29785 5.3589C4.5791 5.02206 4.26562 4.51535 3.82031 4.59443Z" fill="currentColor"/>
<path d="M5.99414 5.59006C5.85352 5.67793 5.7832 5.80973 5.7832 6.00011C5.7832 6.1905 5.85352 6.3223 5.99414 6.41017C6.08789 6.46875 6.14941 6.46875 9.5332 6.46875C12.917 6.46875 12.9785 6.46875 13.0723 6.41017C13.2129 6.3223 13.2832 6.1905 13.2832 6.00011C13.2832 5.80973 13.2129 5.67793 13.0723 5.59006C12.9785 5.53148 12.917 5.53148 9.5332 5.53148C6.14941 5.53148 6.08789 5.53148 5.99414 5.59006Z" fill="black" fill-opacity="0.4"/>
<path d="M3.82031 8.34346C3.75293 8.35517 3.29883 8.64221 2.52539 9.15771C1.86914 9.59706 1.32129 9.95439 1.30957 9.95439C1.29492 9.95439 1.18945 9.76401 1.07227 9.53262C0.837891 9.06691 0.776367 9.00248 0.536133 8.97026C0.436523 8.95854 0.37793 8.97026 0.275391 9.02298C0.0966797 9.11378 0.0117188 9.24851 0.0117188 9.43889C0.0117188 9.5707 0.0498047 9.66735 0.369141 10.3C0.773437 11.1143 0.808594 11.1553 1.08984 11.1553C1.1748 11.1553 1.28613 11.1348 1.33887 11.1084C1.56152 10.9913 4.22754 9.18993 4.29785 9.10792C4.5791 8.77109 4.26562 8.26437 3.82031 8.34346Z" fill="currentColor"/>
<path d="M5.99414 9.80783C5.85352 9.8957 5.7832 10.0275 5.7832 10.2179C5.7832 10.4083 5.85352 10.5401 5.99414 10.6279C6.08789 10.6865 6.14941 10.6865 9.5332 10.6865C12.917 10.6865 12.9785 10.6865 13.0723 10.6279C13.2129 10.5401 13.2832 10.4083 13.2832 10.2179C13.2832 10.0275 13.2129 9.8957 13.0723 9.80783C12.9785 9.74925 12.917 9.74925 9.5332 9.74925C6.14941 9.74925 6.08789 9.74925 5.99414 9.80783Z" fill="black" fill-opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,17 @@
<svg viewBox="0 0 15 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2603_34824)">
<path d="M6.97533 0.54688C6.57396 0.655278 6.1638 0.986333 5.9763 1.35547L5.87377 1.55469H5.33177C4.48216 1.55469 4.45287 1.5752 4.45287 2.11426V2.46289H3.67064C3.23705 2.46289 2.82689 2.47754 2.75365 2.49512C2.26439 2.60645 1.83373 3.01075 1.66088 3.51465L1.59642 3.70801L1.58763 8.90821C1.5847 12.1309 1.59056 14.1846 1.60814 14.3105C1.6433 14.5684 1.79857 14.8877 1.96849 15.0518C2.13548 15.2129 2.40209 15.3711 2.63353 15.4414C2.81517 15.5 2.92943 15.5 7.49974 15.5C12.0701 15.5 12.1843 15.5 12.366 15.4414C12.5974 15.3711 12.864 15.2129 13.031 15.0518C13.2009 14.8877 13.3562 14.5684 13.3913 14.3105C13.4089 14.1846 13.4148 12.1309 13.4118 8.90821L13.4031 3.70801L13.3386 3.51465C13.1687 3.01954 12.7673 2.63282 12.2898 2.50684C12.155 2.47168 11.9411 2.46289 11.3318 2.46289H10.5466V2.11426C10.5466 1.57227 10.5202 1.55469 9.65013 1.55469H9.09056L8.99388 1.35547C8.80052 0.962895 8.41673 0.664067 7.95384 0.54395C7.73412 0.488286 7.19212 0.488286 6.97533 0.54688ZM7.83959 1.25879C8.12084 1.38477 8.3347 1.63379 8.42259 1.94141C8.45482 2.06153 8.49876 2.13477 8.57201 2.19629L8.67455 2.28711H9.24291H9.81419V2.99024V3.69336H7.49974H5.18529V2.99024V2.28711H5.72142C6.3513 2.28711 6.40404 2.26954 6.489 2.03223C6.5974 1.73633 6.65013 1.64258 6.77025 1.51075C7.05443 1.20313 7.48216 1.10059 7.83959 1.25879ZM4.45287 3.67871C4.45287 4.24414 4.47338 4.3086 4.66673 4.39942C4.78392 4.45215 4.90111 4.45508 7.47044 4.45508C9.14916 4.45508 10.1892 4.44336 10.2566 4.42579C10.3122 4.41114 10.3972 4.35547 10.447 4.30567L10.532 4.21778L10.5408 3.70508L10.5495 3.19239L11.3171 3.20118C12.0554 3.20996 12.0906 3.21289 12.2165 3.27735C12.3865 3.36817 12.4685 3.44434 12.5622 3.60547L12.6413 3.73731L12.6472 9.01074L12.656 14.2842L12.5886 14.4014C12.5154 14.5303 12.2986 14.6914 12.1345 14.7383C11.9851 14.7793 3.01439 14.7793 2.86498 14.7383C2.70091 14.6914 2.48412 14.5303 2.41088 14.4014L2.34349 14.2842V9.0459C2.34349 4.40821 2.34935 3.7959 2.38744 3.70215C2.48119 3.4795 2.65697 3.3125 2.88548 3.23047C2.93822 3.21289 3.26341 3.19825 3.71459 3.19825L4.45287 3.19532V3.67871Z" fill="currentColor"/>
<path d="M5.54231 5.66211C5.45149 5.74707 5.18196 6.00488 4.9388 6.2334C4.69856 6.46484 4.49349 6.65234 4.48177 6.65234C4.47298 6.65234 4.34114 6.52637 4.19173 6.37402C3.89583 6.0752 3.81087 6.03125 3.62337 6.09277C3.49446 6.13672 3.42708 6.20996 3.39778 6.34473C3.35091 6.55273 3.40071 6.63184 3.87825 7.1123C4.32063 7.56055 4.32063 7.56055 4.45247 7.56055C4.52571 7.55762 4.61653 7.54004 4.65169 7.51953C4.7513 7.4668 6.13411 6.1543 6.1927 6.06055C6.25423 5.96387 6.25423 5.79102 6.1927 5.68555C6.12239 5.57129 6.00227 5.50977 5.84407 5.50977C5.72102 5.50977 5.69466 5.52441 5.54231 5.66211Z" fill="currentColor"/>
<path d="M6.89584 6.48535C6.70834 6.65527 6.73471 6.93066 6.95151 7.0625C7.04233 7.11816 7.11557 7.12109 9.20151 7.12109H11.3578L11.4574 7.05371C11.6595 6.91895 11.6712 6.62305 11.4808 6.46484L11.39 6.38867H9.19565H7.00131L6.89584 6.48535Z" fill="currentColor" fill-opacity="0.4"/>
<path d="M5.70063 8.39597C5.65669 8.4194 5.36372 8.68601 5.05024 8.98483L4.48481 9.52976L4.24458 9.27487C3.97504 8.99069 3.89301 8.93796 3.72309 8.93796C3.50336 8.93796 3.34516 9.15769 3.3979 9.39206C3.41547 9.47116 3.51801 9.59714 3.8227 9.91355C4.32954 10.438 4.42915 10.4966 4.63715 10.3882C4.7309 10.3384 6.19868 8.95261 6.22212 8.88815C6.2309 8.86472 6.23969 8.78855 6.23969 8.72116C6.23969 8.45163 5.94087 8.27292 5.70063 8.39597Z" fill="currentColor"/>
<path d="M6.98147 9.27441C6.72952 9.38281 6.70608 9.7666 6.93752 9.9248C7.01077 9.97461 7.1426 9.97754 9.19045 9.97754C11.3408 9.97754 11.3643 9.97754 11.4463 9.91602C11.6895 9.7373 11.666 9.37988 11.4053 9.27148C11.2647 9.20996 7.11917 9.21289 6.98147 9.27441Z" fill="currentColor" fill-opacity="0.4"/>
<path d="M5.3292 11.5801C5.12119 11.7793 4.84287 12.0371 4.71397 12.1572L4.47959 12.377L4.19248 12.0898C3.87608 11.7764 3.79112 11.7354 3.59776 11.8174C3.46006 11.876 3.39268 11.9785 3.39268 12.1396C3.38975 12.3213 3.44834 12.4062 3.92002 12.8721C4.30967 13.2559 4.33018 13.2734 4.44737 13.2734C4.51182 13.2734 4.59678 13.2617 4.63487 13.2471C4.71104 13.2178 6.1085 11.8965 6.1876 11.7822C6.25205 11.6855 6.25498 11.5068 6.19346 11.3984C6.12315 11.2842 6.00303 11.2227 5.84776 11.2227C5.71299 11.2227 5.7042 11.2285 5.3292 11.5801Z" fill="currentColor"/>
<path d="M6.89584 12.1982C6.70834 12.3682 6.73471 12.6436 6.95151 12.7754C7.04233 12.8311 7.11557 12.834 9.20151 12.834H11.3578L11.4574 12.7666C11.6595 12.6318 11.6712 12.3359 11.4808 12.1777L11.39 12.1016H9.19565H7.00131L6.89584 12.1982Z" fill="currentColor" fill-opacity="0.4"/>
</g>
<defs>
<clipPath id="clip0_2603_34824">
<rect width="15" height="15" fill="white" transform="translate(0 0.5)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -1 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor"><path stroke="currentColor" stroke-width=".3" d="M8 7.77a2.888 2.888 0 0 0 2.885-2.885A2.888 2.888 0 0 0 8 2a2.888 2.888 0 0 0-2.885 2.885A2.888 2.888 0 0 0 8 7.769Zm0-5.09c1.216 0 2.204.99 2.204 2.205A2.207 2.207 0 0 1 8 7.089a2.207 2.207 0 0 1-2.204-2.204c0-1.216.989-2.205 2.204-2.205ZM10.635 8.658a1.098 1.098 0 0 0-.85.108 3.516 3.516 0 0 1-3.557 0 1.098 1.098 0 0 0-.849-.108c-1.242.358-2.11 1.59-2.11 2.995v1.023c0 .943.703 1.71 1.566 1.71h6.344c.864 0 1.566-.767 1.566-1.71v-1.023c0-1.405-.868-2.637-2.11-2.995Zm1.516 4.018c0 .585-.436 1.062-.972 1.062H4.835c-.536 0-.972-.477-.972-1.062v-1.023c0-1.111.686-2.085 1.668-2.368a.55.55 0 0 1 .42.053 4.064 4.064 0 0 0 4.112 0 .543.543 0 0 1 .42-.053c.982.283 1.668 1.257 1.668 2.368v1.023Z"/><path stroke="currentColor" stroke-width=".5" d="M13.5 9c-.827 0-1.5-.673-1.5-1.5S12.673 6 13.5 6s1.5.673 1.5 1.5S14.327 9 13.5 9Zm0-2.646c-.632 0-1.146.514-1.146 1.146 0 .632.514 1.146 1.146 1.146.632 0 1.146-.514 1.146-1.146 0-.632-.514-1.146-1.146-1.146Z"/><path stroke="currentColor" stroke-width=".6" d="M13.413 10.37a1.66 1.66 0 0 1-.784-.196c-.051-.027-.069-.068-.202-.035l.08.266c.283.15.588.226.906.226.318 0 .623-.076.906-.226a.26.26 0 0 1 .185-.021.988.988 0 0 1 .735.954v.413a.429.429 0 0 1-.429.428h-2.25v.261h2.25c.38 0 .69-.31.69-.69v-.412a1.25 1.25 0 0 0-.93-1.207.523.523 0 0 0-.373.043 1.66 1.66 0 0 1-.784.195Z"/><path stroke="currentColor" stroke-width=".5" d="M2.5 9C3.327 9 4 8.327 4 7.5S3.327 6 2.5 6 1 6.673 1 7.5 1.673 9 2.5 9Zm0-2.646c.632 0 1.146.514 1.146 1.146 0 .632-.514 1.146-1.146 1.146A1.148 1.148 0 0 1 1.354 7.5c0-.632.514-1.146 1.146-1.146Z"/><path stroke="currentColor" stroke-width=".6" d="M2.587 10.37c.275 0 .539-.066.784-.196.051-.027.069-.068.202-.035l-.08.266c-.283.15-.588.226-.906.226-.318 0-.623-.076-.906-.226a.259.259 0 0 0-.185-.021.988.988 0 0 0-.735.954v.413c0 .236.193.428.429.428h2.25v.261H1.19a.69.69 0 0 1-.69-.69v-.412c0-.566.382-1.063.93-1.207a.523.523 0 0 1 .373.043c.245.13.509.195.784.195Z"/></svg>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.17969 3.34032C7.48047 3.68016 6.16406 5.00829 5.83594 6.71141C5.76562 7.0825 5.76562 7.91844 5.83594 8.28954C6.03516 9.33641 6.66406 10.3364 7.49219 10.9223C7.58203 10.9848 7.65625 11.0434 7.65625 11.0552C7.65625 11.0669 7.53125 11.1255 7.37891 11.188C6.96094 11.3598 6.32031 11.8052 5.9375 12.1841C5.57812 12.5473 5.15234 13.145 4.96875 13.5552C4.63672 14.3013 4.53125 14.8325 4.53125 15.7661V16.4536L4.66797 16.5903C4.80078 16.7231 4.81641 16.727 5.02734 16.7114C5.21484 16.6958 5.26562 16.6723 5.35938 16.5669C5.46094 16.4536 5.46875 16.4223 5.46875 16.1216C5.46875 15.5903 5.53906 14.9223 5.62891 14.6098C5.80469 13.9653 6.15625 13.3677 6.63672 12.8872C7.11719 12.4067 7.71484 12.0552 8.35938 11.8794C8.89062 11.7309 10.0117 11.6763 10.918 11.7583C11.8867 11.8442 12.6914 12.2153 13.3633 12.8872C13.8438 13.3677 14.1953 13.9653 14.3711 14.6098C14.4609 14.9223 14.5312 15.5942 14.5312 16.1255C14.5312 16.4497 14.5352 16.4575 14.668 16.5903C14.8008 16.7231 14.8164 16.727 15.0273 16.7114C15.2148 16.6958 15.2656 16.6723 15.3594 16.5669L15.4688 16.4458V15.7622C15.4688 14.8325 15.3633 14.2973 15.0312 13.5552C14.8477 13.145 14.4219 12.5473 14.0625 12.1841C13.6797 11.8052 13.0391 11.3598 12.6211 11.188C12.4688 11.1255 12.3438 11.0669 12.3438 11.0552C12.3438 11.0434 12.418 10.9848 12.5078 10.9223C13.3359 10.3364 13.9648 9.33641 14.1641 8.28954C14.2344 7.91844 14.2344 7.0825 14.1641 6.71141C13.8359 4.99266 12.5078 3.66454 10.7891 3.33641C10.4336 3.27 9.52734 3.27 9.17969 3.34032ZM10.7617 4.34032C11.7188 4.57079 12.5195 5.23094 12.9492 6.145C13.3516 6.98875 13.3477 8.0161 12.9492 8.86375C12.3945 10.0317 11.2773 10.7466 10 10.7466C8.72266 10.7466 7.60547 10.0317 7.05078 8.85594C6.65234 8.0161 6.65234 6.98485 7.04688 6.145C7.37891 5.44969 7.88672 4.92625 8.57422 4.5786C9.23828 4.24657 10.0234 4.16063 10.7617 4.34032Z" fill="#757575"/>
<path d="M5.05859 4.37547C4.08594 4.895 3.34766 5.83641 3.04297 6.93797C2.91797 7.39891 2.89453 8.28954 3 8.78954C3.16016 9.56688 3.50391 10.2192 4.05078 10.7856L4.37891 11.1255L4.05469 11.352C3.61328 11.6567 3.14844 12.1372 2.83984 12.602C2.28125 13.4419 2.03125 14.2778 2.03125 15.3013V15.8286L2.16797 15.9653C2.30078 16.0981 2.31641 16.102 2.52344 16.0864C2.69141 16.0708 2.76172 16.0434 2.84375 15.9653C2.94141 15.8638 2.94922 15.8286 2.99219 15.2388C3.05859 14.3403 3.25 13.7466 3.66406 13.1372C4.09766 12.5005 4.87109 11.8911 5.51172 11.6841C5.70703 11.6216 5.86328 11.4458 5.88281 11.2661C5.91406 11.0239 5.80859 10.8755 5.44531 10.6489C4.83203 10.2622 4.52344 9.93407 4.23438 9.35594C3.99609 8.87547 3.90625 8.48875 3.90625 7.96922C3.90625 7.11766 4.22656 6.35985 4.82812 5.77391C5.11328 5.49657 5.19922 5.43407 5.48047 5.27391C5.73828 5.12938 5.84766 5.00829 5.88281 4.8286C5.91016 4.66454 5.82031 4.44188 5.6875 4.35594C5.5 4.23485 5.31641 4.23875 5.05859 4.37547Z" fill="#757575"/>
<path d="M14.3754 4.32884C14.1996 4.41478 14.0824 4.63743 14.1176 4.82103C14.1527 5.01243 14.266 5.13353 14.5629 5.29759C14.8793 5.47337 15.4691 6.0515 15.6605 6.37571C16.3129 7.4929 16.2308 8.84056 15.4496 9.8679C15.1996 10.1921 14.9613 10.4031 14.5433 10.657C14.3363 10.7859 14.2152 10.8913 14.1683 10.9812C14.0238 11.2546 14.1723 11.5827 14.4887 11.6843C14.9301 11.8288 15.4613 12.1726 15.8637 12.5749C16.2113 12.9304 16.4379 13.2546 16.6488 13.696C16.8637 14.157 16.9496 14.5398 17.0043 15.2507L17.0551 15.8679L17.184 15.9851C17.3051 16.0945 17.3363 16.1023 17.5316 16.0867C17.7152 16.071 17.766 16.0476 17.8598 15.9421C17.9691 15.821 17.9691 15.8171 17.9691 15.2976C17.9691 14.7234 17.9066 14.2976 17.7465 13.8093C17.4301 12.8288 16.7699 11.9265 15.9457 11.3523L15.6215 11.1257L15.9496 10.7859C16.6957 10.0124 17.0707 9.07884 17.0707 7.98509C17.0707 7.46556 17.0355 7.20384 16.8949 6.739C16.6019 5.77415 15.848 4.86009 14.9457 4.37962C14.684 4.2429 14.5668 4.23118 14.3754 4.32884Z" fill="#757575"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -1 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"><path stroke="currentColor" stroke-width=".2" d="M7.998 7.31a3.409 3.409 0 0 0 3.405-3.405A3.409 3.409 0 0 0 7.998.5a3.409 3.409 0 0 0-3.404 3.405A3.409 3.409 0 0 0 7.998 7.31Zm0-6.007A2.605 2.605 0 0 1 10.6 3.905a2.605 2.605 0 0 1-2.602 2.602 2.605 2.605 0 0 1-2.602-2.602 2.605 2.605 0 0 1 2.602-2.602ZM11.552 8.409c-.38-.1-.798-.052-1.148.132a5.096 5.096 0 0 1-2.406.6 5.096 5.096 0 0 1-2.406-.6 1.607 1.607 0 0 0-1.148-.132 3.835 3.835 0 0 0-2.854 3.707v1.266A2.12 2.12 0 0 0 3.708 15.5h8.58a2.12 2.12 0 0 0 2.118-2.118v-1.266a3.835 3.835 0 0 0-2.854-3.707Zm2.052 4.973c0 .725-.59 1.315-1.315 1.315H3.708c-.725 0-1.315-.59-1.315-1.315v-1.266c0-1.376.928-2.58 2.256-2.931a.798.798 0 0 1 .568.066 5.89 5.89 0 0 0 2.781.692 5.89 5.89 0 0 0 2.782-.692.795.795 0 0 1 .568-.066 3.032 3.032 0 0 1 2.256 2.93v1.267Z"/></svg>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.17969 3.34032C7.48047 3.68016 6.16406 5.00829 5.83594 6.71141C5.76562 7.0825 5.76562 7.91844 5.83594 8.28954C6.03516 9.33641 6.66406 10.3364 7.49219 10.9223C7.58203 10.9848 7.65625 11.0434 7.65625 11.0552C7.65625 11.0669 7.53125 11.1255 7.37891 11.188C6.96094 11.3598 6.32031 11.8052 5.9375 12.1841C5.57812 12.5473 5.15234 13.145 4.96875 13.5552C4.63672 14.3013 4.53125 14.8325 4.53125 15.7661V16.4536L4.66797 16.5903C4.80078 16.7231 4.81641 16.727 5.02734 16.7114C5.21484 16.6958 5.26562 16.6723 5.35938 16.5669C5.46094 16.4536 5.46875 16.4223 5.46875 16.1216C5.46875 15.5903 5.53906 14.9223 5.62891 14.6098C5.80469 13.9653 6.15625 13.3677 6.63672 12.8872C7.11719 12.4067 7.71484 12.0552 8.35938 11.8794C8.89062 11.7309 10.0117 11.6763 10.918 11.7583C11.8867 11.8442 12.6914 12.2153 13.3633 12.8872C13.8438 13.3677 14.1953 13.9653 14.3711 14.6098C14.4609 14.9223 14.5312 15.5942 14.5312 16.1255C14.5312 16.4497 14.5352 16.4575 14.668 16.5903C14.8008 16.7231 14.8164 16.727 15.0273 16.7114C15.2148 16.6958 15.2656 16.6723 15.3594 16.5669L15.4688 16.4458V15.7622C15.4688 14.8325 15.3633 14.2973 15.0312 13.5552C14.8477 13.145 14.4219 12.5473 14.0625 12.1841C13.6797 11.8052 13.0391 11.3598 12.6211 11.188C12.4688 11.1255 12.3438 11.0669 12.3438 11.0552C12.3438 11.0434 12.418 10.9848 12.5078 10.9223C13.3359 10.3364 13.9648 9.33641 14.1641 8.28954C14.2344 7.91844 14.2344 7.0825 14.1641 6.71141C13.8359 4.99266 12.5078 3.66454 10.7891 3.33641C10.4336 3.27 9.52734 3.27 9.17969 3.34032ZM10.7617 4.34032C11.7188 4.57079 12.5195 5.23094 12.9492 6.145C13.3516 6.98875 13.3477 8.0161 12.9492 8.86375C12.3945 10.0317 11.2773 10.7466 10 10.7466C8.72266 10.7466 7.60547 10.0317 7.05078 8.85594C6.65234 8.0161 6.65234 6.98485 7.04688 6.145C7.37891 5.44969 7.88672 4.92625 8.57422 4.5786C9.23828 4.24657 10.0234 4.16063 10.7617 4.34032Z" fill="#757575"/>
</svg>

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -10,7 +10,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Menu, Typography } from 'antd';
import { Menu, Space, Typography } from 'antd';
import classNames from 'classnames';
import Loader from 'components/Loader/Loader';
import { TaskTab } from 'components/Task/TaskTab/TaskTab.component';
@ -53,6 +53,11 @@ import {
import { ReactComponent as CheckIcon } from '/assets/svg/ic-check.svg';
import { ReactComponent as TaskIcon } from '/assets/svg/ic-task.svg';
import { ICON_DIMENSION } from 'constants/constants';
import { ReactComponent as AllActivityIcon } from '/assets/svg/all-activity-v2.svg';
import { ReactComponent as MentionIcon } from '/assets/svg/ic-mentions.svg';
import { ReactComponent as TaskListIcon } from '/assets/svg/task-ic.svg';
export const ActivityFeedTab = ({
fqn,
owner,
@ -280,7 +285,11 @@ export const ActivityFeedTab = ({
{
label: (
<div className="d-flex justify-between">
<span>{t('label.all')}</span>
<Space align="center" size="small">
<AllActivityIcon {...ICON_DIMENSION} />
<span>{t('label.all')}</span>
</Space>
<span>
{getCountBadge(
allCount,
@ -294,16 +303,20 @@ export const ActivityFeedTab = ({
},
{
label: (
<div className="d-flex justify-between">
<Space align="center" size="small">
<MentionIcon {...ICON_DIMENSION} />
<span>{t('label.mention-plural')}</span>
</div>
</Space>
),
key: 'mentions',
},
{
label: (
<div className="d-flex justify-between">
<span>{t('label.task-plural')}</span>
<Space align="center" size="small">
<TaskListIcon {...ICON_DIMENSION} />
<span>{t('label.task-plural')}</span>
</Space>
<span>
{getCountBadge(
tasksCount,

View File

@ -19,6 +19,12 @@
.activity-feed-tab {
display: flex;
.custom-menu.ant-menu-root.ant-menu-inline {
.ant-menu-item {
height: 40px;
}
}
.center-container {
flex: 0 0 calc(50% - @left-side-panel-width / 2);
height: @entity-details-tab-height;

View File

@ -22,12 +22,4 @@
.entity-summary-details {
font-size: 12px;
}
.max-two-lines {
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}
}

View File

@ -16,4 +16,5 @@ export interface TeamsSelectableProps {
filterJoinable?: boolean;
placeholder?: string;
selectedTeams?: string[];
maxValueCount?: number;
}

View File

@ -30,6 +30,7 @@ const TeamsSelectable = ({
type: t('label.team-plural-lowercase'),
}),
selectedTeams,
maxValueCount,
}: TeamsSelectableProps) => {
const [value, setValue] = useState<Array<string>>();
const [noTeam, setNoTeam] = useState<boolean>(false);
@ -93,6 +94,7 @@ const TeamsSelectable = ({
treeDefaultExpandAll
data-testid="team-select"
dropdownStyle={{ maxHeight: 300, overflow: 'auto' }}
maxTagCount={maxValueCount}
placeholder={placeholder}
showCheckedStrategy={TreeSelect.SHOW_CHILD}
style={{ width: '100%' }}

View File

@ -22,7 +22,7 @@ type ChangePasswordForm = {
visible: boolean;
onCancel: () => void;
onSave: (data: ChangePasswordRequest) => void;
isLoggedinUser: boolean;
isLoggedInUser: boolean;
isLoading: boolean;
};
@ -30,7 +30,7 @@ const ChangePasswordForm: React.FC<ChangePasswordForm> = ({
visible,
onCancel,
onSave,
isLoggedinUser,
isLoggedInUser,
isLoading,
}) => {
const { t } = useTranslation();
@ -65,7 +65,7 @@ const ChangePasswordForm: React.FC<ChangePasswordForm> = ({
name="change-password-form"
validateMessages={VALIDATION_MESSAGES}
onFinish={onSave}>
{isLoggedinUser && (
{isLoggedInUser && (
<Form.Item
label={t('label.old-password')}
name="oldPassword"

View File

@ -12,11 +12,10 @@
*/
import {
act,
findByTestId,
findByText,
queryByTestId,
render,
screen,
} from '@testing-library/react';
import React, { ReactNode } from 'react';
import { MemoryRouter } from 'react-router-dom';
@ -48,6 +47,32 @@ jest.mock('../common/ProfilePicture/ProfilePicture', () => {
return jest.fn().mockReturnValue(<p>ProfilePicture</p>);
});
jest.mock(
'./UsersProfile/UserProfileDetails/UserProfileDetails.component',
() => {
return jest.fn().mockReturnValue(<div>UserProfileDetails</div>);
}
);
jest.mock('./UsersProfile/UserProfileImage/UserProfileImage.component', () => {
return jest.fn().mockReturnValue(<div>UserProfileImage</div>);
});
jest.mock(
'./UsersProfile/UserProfileInheritedRoles/UserProfileInheritedRoles.component',
() => {
return jest.fn().mockReturnValue(<div>UserProfileInheritedRoles</div>);
}
);
jest.mock('./UsersProfile/UserProfileRoles/UserProfileRoles.component', () => {
return jest.fn().mockReturnValue(<div>UserProfileRoles</div>);
});
jest.mock('./UsersProfile/UserProfileTeams/UserProfileTeams.component', () => {
return jest.fn().mockReturnValue(<div>UserProfileTeams</div>);
});
jest.mock('components/searched-data/SearchedData', () => {
return jest.fn().mockReturnValue(<p>SearchedData</p>);
});
@ -132,7 +157,7 @@ const mockProp = {
paging: mockPaging,
postFeedHandler: postFeed,
isAdminUser: false,
isLoggedinUser: false,
isLoggedInUser: false,
isAuthDisabled: true,
isUserEntitiesLoading: false,
updateUserDetails,
@ -166,28 +191,24 @@ describe('Test User Component', () => {
}
);
const leftPanel = await findByTestId(container, 'left-panel');
expect(leftPanel).toBeInTheDocument();
});
it('Only admin can able to see tab for bot page', async () => {
const { container } = render(
<Users
userData={{ ...mockUserData, isBot: true }}
{...mockProp}
isAdminUser
/>,
{
wrapper: MemoryRouter,
}
const UserProfileDetails = await findByText(
container,
'UserProfileDetails'
);
const UserProfileImage = await findByText(container, 'UserProfileImage');
const UserProfileInheritedRoles = await findByText(
container,
'UserProfileInheritedRoles'
);
const UserProfileRoles = await findByText(container, 'UserProfileRoles');
const tabs = await findByTestId(container, 'tabs');
const leftPanel = await findByTestId(container, 'left-panel');
const UserProfileTeams = await findByText(container, 'UserProfileTeams');
expect(tabs).toBeInTheDocument();
expect(leftPanel).toBeInTheDocument();
expect(UserProfileDetails).toBeInTheDocument();
expect(UserProfileImage).toBeInTheDocument();
expect(UserProfileRoles).toBeInTheDocument();
expect(UserProfileTeams).toBeInTheDocument();
expect(UserProfileInheritedRoles).toBeInTheDocument();
});
it('Tab should not visible to normal user', async () => {
@ -199,36 +220,8 @@ describe('Test User Component', () => {
);
const tabs = queryByTestId(container, 'tab');
const leftPanel = await findByTestId(container, 'left-panel');
expect(tabs).not.toBeInTheDocument();
expect(leftPanel).toBeInTheDocument();
});
it('Should render non deleted teams', async () => {
const { container } = render(
<Users userData={mockUserData} {...mockProp} />,
{
wrapper: MemoryRouter,
}
);
const teamFinance = await findByTestId(container, 'Finance');
const teamDataPlatform = await findByTestId(container, 'Data_Platform');
expect(teamFinance).toBeInTheDocument();
expect(teamDataPlatform).toBeInTheDocument();
});
it('Should not render deleted teams', async () => {
await act(async () => {
render(<Users userData={mockUserData} {...mockProp} />, {
wrapper: MemoryRouter,
});
});
const deletedTeam = screen.queryByTestId('Customer_Support');
expect(deletedTeam).not.toBeInTheDocument();
});
it('Should check if cards are rendered', async () => {
@ -245,19 +238,6 @@ describe('Test User Component', () => {
expect(datasetContainer).toBeInTheDocument();
});
it('Should render inherited roles', async () => {
mockParams.tab = UserPageTabs.FOLLOWING;
const { container } = render(
<Users userData={mockUserData} {...mockProp} />,
{
wrapper: MemoryRouter,
}
);
const inheritedRoles = await findByTestId(container, 'inherited-roles');
expect(inheritedRoles).toBeInTheDocument();
});
it('MyData tab should show loader if the data is loading', async () => {
mockParams.tab = UserPageTabs.MY_DATA;
const { container } = render(

View File

@ -11,74 +11,32 @@
* limitations under the License.
*/
import {
Card,
Col,
Image,
Input,
Row,
Select,
Space,
Tabs,
Typography,
} from 'antd';
import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg';
import { ReactComponent as IconTeamsGrey } from 'assets/svg/teams-grey.svg';
import { AxiosError } from 'axios';
import { Col, Row, Tabs, Typography } from 'antd';
import ActivityFeedProvider from 'components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider';
import { ActivityFeedTab } from 'components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component';
import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlaceHolder';
import EntitySummaryPanel from 'components/Explore/EntitySummaryPanel/EntitySummaryPanel.component';
import InlineEdit from 'components/InlineEdit/InlineEdit.component';
import SearchedData from 'components/searched-data/SearchedData';
import { SearchedDataProps } from 'components/searched-data/SearchedData.interface';
import TabsLabel from 'components/TabsLabel/TabsLabel.component';
import TeamsSelectable from 'components/TeamsSelectable/TeamsSelectable';
import { EntityType } from 'enums/entity.enum';
import { isEmpty, noop, toLower } from 'lodash';
import { isEmpty, noop } from 'lodash';
import { observer } from 'mobx-react';
import React, {
Fragment,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory, useLocation, useParams } from 'react-router-dom';
import { changePassword } from 'rest/auth-API';
import { getRoles } from 'rest/rolesAPIV1';
import { getEntityName } from 'utils/EntityUtils';
import {
DE_ACTIVE_COLOR,
getUserPath,
PAGE_SIZE_LARGE,
TERM_ADMIN,
} from '../../constants/constants';
import { getUserPath } from '../../constants/constants';
import { USER_PROFILE_TABS } from '../../constants/usersprofile.constants';
import { AuthTypes } from '../../enums/signin.enum';
import {
ChangePasswordRequest,
RequestType,
} from '../../generated/auth/changePasswordRequest';
import { Role } from '../../generated/entity/teams/role';
import { EntityReference } from '../../generated/entity/teams/user';
import { getNonDeletedTeams } from '../../utils/CommonUtils';
import {
getImageWithResolutionAndFallback,
ImageQuality,
} from '../../utils/ProfilerUtils';
import SVGIcons, { Icons } from '../../utils/SvgUtils';
import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
import { useAuthContext } from '../authentication/auth-provider/AuthProvider';
import Description from '../common/description/Description';
import ProfilePicture from '../common/ProfilePicture/ProfilePicture';
import PageLayoutV1 from '../containers/PageLayoutV1';
import Loader from '../Loader/Loader';
import ChangePasswordForm from './ChangePasswordForm';
import { Props, UserPageTabs } from './Users.interface';
import './Users.style.less';
import { userPageFilterList } from './Users.util';
import UserProfileDetails from './UsersProfile/UserProfileDetails/UserProfileDetails.component';
import UserProfileImage from './UsersProfile/UserProfileImage/UserProfileImage.component';
import UserProfileInheritedRoles from './UsersProfile/UserProfileInheritedRoles/UserProfileInheritedRoles.component';
import UserProfileRoles from './UsersProfile/UserProfileRoles/UserProfileRoles.component';
import UserProfileTeams from './UsersProfile/UserProfileTeams/UserProfileTeams.component';
const Users = ({
userData,
@ -86,43 +44,20 @@ const Users = ({
ownedEntities,
isUserEntitiesLoading,
updateUserDetails,
isAdminUser,
isLoggedinUser,
isAuthDisabled,
username,
handlePaginate,
}: Props) => {
const { tab = UserPageTabs.ACTIVITY } = useParams<{ tab: UserPageTabs }>();
const [displayName, setDisplayName] = useState(userData.displayName);
const [isDisplayNameEdit, setIsDisplayNameEdit] = useState(false);
const [isDescriptionEdit, setIsDescriptionEdit] = useState(false);
const [isRolesEdit, setIsRolesEdit] = useState(false);
const [isTeamsEdit, setIsTeamsEdit] = useState(false);
const [selectedRoles, setSelectedRoles] = useState<Array<string>>([]);
const [selectedTeams, setSelectedTeams] = useState<Array<string>>([]);
const [roles, setRoles] = useState<Array<Role>>([]);
const history = useHistory();
const [isImgUrlValid, SetIsImgUrlValid] = useState<boolean>(true);
const [isChangePassword, setIsChangePassword] = useState<boolean>(false);
const location = useLocation();
const [isLoading, setIsLoading] = useState(false);
const [isRolesLoading, setIsRolesLoading] = useState<boolean>(false);
const [showSummaryPanel, setShowSummaryPanel] = useState(false);
const [entityDetails, setEntityDetails] =
useState<SearchedDataProps['data'][number]['_source']>();
const { authConfig } = useAuthContext();
const { t } = useTranslation();
const { isAuthProviderBasic } = useMemo(() => {
return {
isAuthProviderBasic:
authConfig?.provider === AuthTypes.BASIC ||
authConfig?.provider === AuthTypes.LDAP,
};
}, [authConfig]);
const tabs = useMemo(() => {
return USER_PROFILE_TABS.map((data) => ({
label: <TabsLabel id={data.key} key={data.key} name={data.name} />,
@ -130,10 +65,6 @@ const Users = ({
}));
}, []);
const onDisplayNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setDisplayName(e.target.value);
};
const activeTabHandler = (activeKey: string) => {
// To reset search params appends from other page for proper navigation
location.search = '';
@ -145,508 +76,6 @@ const Users = ({
}
};
const handleDisplayNameChange = () => {
if (displayName !== userData.displayName) {
updateUserDetails({ displayName: displayName || '' });
}
setIsDisplayNameEdit(false);
};
const handleDescriptionChange = async (description: string) => {
await updateUserDetails({ description });
setIsDescriptionEdit(false);
};
const handleRolesChange = () => {
// filter out the roles , and exclude the admin one
const updatedRoles = selectedRoles.filter(
(roleId) => roleId !== toLower(TERM_ADMIN)
);
// get the admin role and send it as boolean value `isAdmin=Boolean(isAdmin)
const isAdmin = selectedRoles.find(
(roleId) => roleId === toLower(TERM_ADMIN)
);
updateUserDetails({
roles: updatedRoles.map((roleId) => {
const role = roles.find((r) => r.id === roleId);
return { id: roleId, type: 'role', name: role?.name || '' };
}),
isAdmin: Boolean(isAdmin),
});
setIsRolesEdit(false);
};
const handleTeamsChange = () => {
updateUserDetails({
teams: selectedTeams.map((teamId) => {
return { id: teamId, type: 'team' };
}),
});
setIsTeamsEdit(false);
};
const handleOnRolesChange = (value: string[]) => {
setSelectedRoles(value);
};
const handleOnTeamsChange = (value: string[]) => {
setSelectedTeams(value);
};
const handleChangePassword = async (data: ChangePasswordRequest) => {
try {
setIsLoading(true);
const sendData = {
...data,
...(isAdminUser &&
!isLoggedinUser && {
username: userData.name,
requestType: RequestType.User,
}),
};
await changePassword(sendData);
setIsChangePassword(false);
showSuccessToast(
t('server.update-entity-success', { entity: t('label.password') })
);
} catch (err) {
showErrorToast(err as AxiosError);
} finally {
setIsLoading(true);
}
};
const getDisplayNameComponent = () => {
if (isAdminUser || isLoggedinUser || isAuthDisabled) {
return (
<div className="w-full">
{isDisplayNameEdit ? (
<InlineEdit
direction="vertical"
onCancel={() => setIsDisplayNameEdit(false)}
onSave={handleDisplayNameChange}>
<Input
className="w-full"
data-testid="displayName"
id="displayName"
name="displayName"
placeholder={t('label.display-name')}
type="text"
value={displayName}
onChange={onDisplayNameChange}
/>
</InlineEdit>
) : (
<Fragment>
<span className="tw-text-base tw-font-medium tw-mr-2 tw-overflow-auto">
{userData.displayName ||
t('label.add-entity', { entity: t('label.display-name') })}
</span>
<button
className="tw-ml-2 focus:tw-outline-none"
data-testid="edit-displayName"
onClick={() => setIsDisplayNameEdit(true)}>
<EditIcon color={DE_ACTIVE_COLOR} width={16} />
</button>
</Fragment>
)}
</div>
);
} else {
return (
<p className="tw-mt-2">
{getEntityName(userData as unknown as EntityReference)}
</p>
);
}
};
const getDescriptionComponent = () => {
if (isAdminUser || isLoggedinUser || isAuthDisabled) {
return (
<div className="flex items-center justify-between">
<Description
description={userData.description || ''}
entityName={getEntityName(userData as unknown as EntityReference)}
hasEditAccess={isAdminUser || isLoggedinUser}
isEdit={isDescriptionEdit}
onCancel={() => setIsDescriptionEdit(false)}
onDescriptionEdit={() => setIsDescriptionEdit(true)}
onDescriptionUpdate={handleDescriptionChange}
/>
</div>
);
} else {
return (
<Typography.Paragraph className="m-b-0">
{userData.description || (
<span className="text-grey-muted">
{t('label.no-entity', {
entity: t('label.description'),
})}
</span>
)}
</Typography.Paragraph>
);
}
};
const getChangePasswordComponent = () => {
return (
<div>
<Typography.Text
className="text-primary text-xs cursor-pointer"
onClick={() => setIsChangePassword(true)}>
{t('label.change-entity', { entity: t('label.password-lowercase') })}
</Typography.Text>
<ChangePasswordForm
isLoading={isLoading}
isLoggedinUser={isLoggedinUser}
visible={isChangePassword}
onCancel={() => setIsChangePassword(false)}
onSave={(data) => handleChangePassword(data)}
/>
</div>
);
};
const getTeamsComponent = () => {
const teamsElement = (
<Fragment>
{getNonDeletedTeams(userData.teams ?? []).map((team, i) => (
<div
className="tw-mb-2 d-flex tw-items-center tw-gap-2"
data-testid={team.name}
key={i}>
<IconTeamsGrey height={16} width={16} />
<Typography.Text
className="ant-typography-ellipsis-custom w-48"
ellipsis={{ tooltip: true }}>
{getEntityName(team)}
</Typography.Text>
</div>
))}
{isEmpty(userData.teams) && (
<span className="text-grey-muted ">{t('message.no-team-found')}</span>
)}
</Fragment>
);
if (!isAdminUser && !isAuthDisabled) {
return (
<Card
className="ant-card-feed relative card-body-border-none card-padding-y-0"
key="teams-card"
style={{
marginTop: '20px',
}}
title={
<div className="d-flex tw-items-center tw-justify-between">
<h6 className="right-panel-label tw-mb-0">
{t('label.team-plural')}
</h6>
</div>
}>
<div className="tw-mb-4">{teamsElement}</div>
</Card>
);
} else {
return (
<Card
className="ant-card-feed relative card-body-border-none card-padding-y-0"
key="teams-card"
style={{
marginTop: '20px',
}}
title={
<div className="d-flex tw-items-center tw-justify-between">
<h6 className="right-panel-label tw-mb-0">
{t('label.team-plural')}
</h6>
{!isTeamsEdit && (
<button
className="tw-ml-2 focus:tw-outline-none "
data-testid="edit-teams"
onClick={() => setIsTeamsEdit(true)}>
<EditIcon color={DE_ACTIVE_COLOR} width={16} />
</button>
)}
</div>
}>
<div className="tw-mb-4">
{isTeamsEdit ? (
<InlineEdit
direction="vertical"
onCancel={() => setIsTeamsEdit(false)}
onSave={handleTeamsChange}>
<TeamsSelectable
filterJoinable
selectedTeams={selectedTeams}
onSelectionChange={handleOnTeamsChange}
/>
</InlineEdit>
) : (
teamsElement
)}
</div>
</Card>
);
}
};
const getRolesComponent = () => {
const userRolesOption = roles?.map((role) => ({
label: getEntityName(role as unknown as EntityReference),
value: role.id,
}));
if (!userData.isAdmin) {
userRolesOption.push({
label: TERM_ADMIN,
value: toLower(TERM_ADMIN),
});
}
const rolesElement = (
<Fragment>
{userData.isAdmin && (
<div className="tw-mb-2 d-flex tw-items-center tw-gap-2">
<SVGIcons alt="icon" className="tw-w-4" icon={Icons.USERS} />
<span>{TERM_ADMIN}</span>
</div>
)}
{userData.roles?.map((role, i) => (
<div className="tw-mb-2 d-flex tw-items-center tw-gap-2" key={i}>
<SVGIcons alt="icon" className="tw-w-4" icon={Icons.USERS} />
<Typography.Text
className="ant-typography-ellipsis-custom w-48"
ellipsis={{ tooltip: true }}>
{getEntityName(role)}
</Typography.Text>
</div>
))}
{!userData.isAdmin && isEmpty(userData.roles) && (
<span className="text-grey-muted ">
{t('message.no-roles-assigned')}
</span>
)}
</Fragment>
);
if (!isAdminUser && !isAuthDisabled) {
return (
<Card
className="ant-card-feed relative card-body-border-none card-padding-y-0"
key="roles-card "
style={{
marginTop: '20px',
}}
title={
<div className="d-flex tw-items-center tw-justify-between">
<h6 className="right-panel-label tw-mb-0">
{t('label.role-plural')}
</h6>
</div>
}>
<div className="roles-container">{rolesElement}</div>
</Card>
);
} else {
return (
<Card
className="ant-card-feed relative card-body-border-none card-padding-y-0"
key="roles-card"
style={{
marginTop: '20px',
}}
title={
<div className="d-flex tw-items-center tw-justify-between">
<h6 className="right-panel-label tw-mb-0">
{t('label.role-plural')}
</h6>
{!isRolesEdit && (
<button
className="tw-ml-2 focus:tw-outline-none"
data-testid="edit-roles"
onClick={() => setIsRolesEdit(true)}>
<EditIcon color={DE_ACTIVE_COLOR} width={16} />
</button>
)}
</div>
}>
<div className="tw-mb-4">
{isRolesEdit ? (
<InlineEdit
direction="vertical"
onCancel={() => setIsRolesEdit(false)}
onSave={handleRolesChange}>
<Select
allowClear
showSearch
aria-label="Select roles"
className="w-full"
id="select-role"
loading={isRolesLoading}
mode="multiple"
options={userRolesOption}
placeholder={t('label.role-plural')}
value={!isRolesLoading ? selectedRoles : []}
onChange={handleOnRolesChange}
/>
</InlineEdit>
) : (
rolesElement
)}
</div>
</Card>
);
}
};
const getInheritedRolesComponent = () => {
return (
<Card
className="ant-card-feed relative card-body-border-none card-padding-y-0"
key="inherited-roles-card-component"
style={{
marginTop: '20px',
}}
title={
<div className="d-flex">
<h6
className="right-panel-label tw-mb-0"
data-testid="inherited-roles">
{t('label.inherited-role-plural')}
</h6>
</div>
}>
<Fragment>
{isEmpty(userData.inheritedRoles) ? (
<div className="tw-mb-4">
<span className="text-grey-muted">
{t('message.no-inherited-roles-found')}
</span>
</div>
) : (
<div className="d-flex tw-justify-between flex-col">
{userData.inheritedRoles?.map((inheritedRole, i) => (
<div
className="tw-mb-2 d-flex tw-items-center tw-gap-2"
key={i}>
<SVGIcons alt="icon" className="tw-w-4" icon={Icons.USERS} />
<Typography.Text
className="ant-typography-ellipsis-custom w-48"
ellipsis={{ tooltip: true }}>
{getEntityName(inheritedRole)}
</Typography.Text>
</div>
))}
</div>
)}
</Fragment>
</Card>
);
};
const image = useMemo(
() =>
getImageWithResolutionAndFallback(
ImageQuality['6x'],
userData.profile?.images
),
[userData.profile?.images]
);
const fetchLeftPanel = () => {
return (
<div className="p-xs user-profile-antd-card" data-testid="left-panel">
<Card className="ant-card-feed relative" key="left-panel-card">
{isImgUrlValid ? (
<Image
alt="profile"
className="tw-w-full"
preview={false}
referrerPolicy="no-referrer"
src={image || ''}
onError={() => {
SetIsImgUrlValid(false);
}}
/>
) : (
<div style={{ width: 'inherit' }}>
<ProfilePicture
displayName={userData?.displayName || userData.name}
height="150"
id={userData?.id || ''}
name={userData?.name || ''}
textClass="tw-text-5xl"
width=""
/>
</div>
)}
<Space className="p-sm w-full" direction="vertical" size={8}>
{getDisplayNameComponent()}
<Typography.Paragraph
className="m-b-0"
ellipsis={{ tooltip: true }}>
{userData.email}
</Typography.Paragraph>
{getDescriptionComponent()}
{isAuthProviderBasic &&
(isAdminUser || isLoggedinUser) &&
getChangePasswordComponent()}
</Space>
</Card>
{getTeamsComponent()}
{getRolesComponent()}
{getInheritedRolesComponent()}
</div>
);
};
const prepareSelectedRoles = () => {
const defaultRoles = [...(userData.roles?.map((role) => role.id) || [])];
if (userData.isAdmin) {
defaultRoles.push(toLower(TERM_ADMIN));
}
setSelectedRoles(defaultRoles);
};
const prepareSelectedTeams = () => {
setSelectedTeams(
getNonDeletedTeams(userData.teams || []).map((team) => team.id)
);
};
const fetchRoles = async () => {
setIsRolesLoading(true);
try {
const response = await getRoles(
'',
undefined,
undefined,
false,
PAGE_SIZE_LARGE
);
setRoles(response.data);
} catch (err) {
setRoles([]);
showErrorToast(
err as AxiosError,
t('server.entity-fetch-error', {
entity: t('label.role-plural'),
})
);
} finally {
setIsRolesLoading(false);
}
};
const handleSummaryPanelDisplay = useCallback(
(details: SearchedDataProps['data'][number]['_source']) => {
setShowSummaryPanel(true);
@ -673,23 +102,6 @@ const Users = ({
}
}, [tab, ownedEntities, followingEntities]);
useEffect(() => {
prepareSelectedRoles();
prepareSelectedTeams();
}, [userData]);
useEffect(() => {
if (image) {
SetIsImgUrlValid(true);
}
}, [image]);
useEffect(() => {
if (isRolesEdit && isEmpty(roles)) {
fetchRoles();
}
}, [isRolesEdit, roles]);
const tabDetails = useMemo(() => {
switch (tab) {
case UserPageTabs.FOLLOWING:
@ -763,11 +175,49 @@ const Users = ({
]);
return (
<PageLayoutV1
className="user-layout h-full"
leftPanel={fetchLeftPanel()}
pageTitle={t('label.user')}>
<PageLayoutV1 className="user-layout h-full" pageTitle={t('label.user')}>
<div data-testid="table-container">
<Row className="user-profile-container" data-testid="user-profile">
<Col className="flex-center border-right" span={4}>
<UserProfileImage
userData={{
id: userData.id,
name: userData.name,
displayName: userData.displayName,
images: userData.profile?.images,
}}
/>
</Col>
<Col className="p-x-sm border-right" span={5}>
<UserProfileDetails
updateUserDetails={updateUserDetails}
userData={{
email: userData.email,
name: userData.name,
displayName: userData.displayName,
description: userData.description,
}}
/>
</Col>
<Col className="p-x-sm border-right" span={5}>
<UserProfileTeams
teams={userData.teams}
updateUserDetails={updateUserDetails}
/>
</Col>
<Col className="p-x-sm border-right" span={5}>
<UserProfileRoles
isUserAdmin={userData.isAdmin}
updateUserDetails={updateUserDetails}
userRoles={userData.roles}
/>
</Col>
<Col className="p-x-sm" span={5}>
<UserProfileInheritedRoles
inheritedRoles={userData.inheritedRoles}
/>
</Col>
</Row>
<Tabs
activeKey={tab ?? UserPageTabs.ACTIVITY}
className="user-page-tabs"

View File

@ -26,9 +26,6 @@ export interface Props {
};
username: string;
isUserEntitiesLoading: boolean;
isAdminUser: boolean;
isLoggedinUser: boolean;
isAuthDisabled: boolean;
handlePaginate: (page: string | number) => void;
updateUserDetails: (data: Partial<User>) => Promise<void>;
}

View File

@ -22,29 +22,52 @@
}
.user-page-tabs {
margin-top: 10px;
.ant-tabs-nav {
margin: 0 !important;
padding: 0 20px;
}
}
.user-page-layout {
.user-layout-scroll {
height: @users-page-tabs-height;
overflow-y: scroll;
}
.user-page-layout-right-panel {
padding-right: 0 !important;
background-color: @white;
border: 1px solid @border-color;
border-radius: 0;
padding-left: 0 !important;
border-top: 0;
}
}
.user-layout {
.ant-col {
padding-top: 0;
}
.user-profile-container {
padding: 30px 0;
height: 270px;
background: @user-profile-background;
.profile-image-container {
width: 190px;
height: 190px;
border-radius: 50%;
overflow: hidden;
border: 3px solid @white;
}
.ant-card {
background: none;
}
}
.user-page-layout {
.user-layout-scroll {
height: @users-page-tabs-height;
overflow-y: scroll;
}
.user-page-layout-right-panel {
padding-right: 0 !important;
background-color: @white;
border: 1px solid @border-color;
border-radius: 0;
padding-left: 0 !important;
border-top: 0;
}
}
.activity-feed-tab {
.center-container {
height: @users-page-tabs-height;

View File

@ -0,0 +1,248 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Button, Col, Input, Row, Space, Typography } from 'antd';
import AppState from 'AppState';
import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg';
import { AxiosError } from 'axios';
import { useAuthContext } from 'components/authentication/auth-provider/AuthProvider';
import DescriptionV1 from 'components/common/description/DescriptionV1';
import InlineEdit from 'components/InlineEdit/InlineEdit.component';
import ChangePasswordForm from 'components/Users/ChangePasswordForm';
import { DE_ACTIVE_COLOR, ICON_DIMENSION } from 'constants/constants';
import { EntityType } from 'enums/entity.enum';
import { AuthTypes } from 'enums/signin.enum';
import {
ChangePasswordRequest,
RequestType,
} from 'generated/auth/changePasswordRequest';
import { EntityReference } from 'generated/entity/type';
import { useAuth } from 'hooks/authHooks';
import React, { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import { changePassword } from 'rest/auth-API';
import { getEntityName } from 'utils/EntityUtils';
import { showErrorToast, showSuccessToast } from 'utils/ToastUtils';
import { UserProfileDetailsProps } from './UserProfileDetails.interface';
const UserProfileDetails = ({
userData,
updateUserDetails,
}: UserProfileDetailsProps) => {
const { t } = useTranslation();
const { username } = useParams<{ [key: string]: string }>();
const { isAdminUser } = useAuth();
const { authConfig } = useAuthContext();
const [isLoading, setIsLoading] = useState(false);
const [isChangePassword, setIsChangePassword] = useState<boolean>(false);
const [displayName, setDisplayName] = useState(userData.displayName);
const [isDisplayNameEdit, setIsDisplayNameEdit] = useState(false);
const [isDescriptionEdit, setIsDescriptionEdit] = useState(false);
const isAuthProviderBasic = useMemo(
() =>
authConfig?.provider === AuthTypes.BASIC ||
authConfig?.provider === AuthTypes.LDAP,
[authConfig]
);
const isLoggedInUser = useMemo(
() => username === AppState.getCurrentUserDetails()?.name,
[username, AppState.nonSecureUserDetails, AppState.userDetails]
);
const hasEditPermission = useMemo(
() => isAdminUser || isLoggedInUser,
[isAdminUser, isLoggedInUser]
);
const onDisplayNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setDisplayName(e.target.value);
};
const handleDescriptionChange = async (description: string) => {
await updateUserDetails({ description });
setIsDescriptionEdit(false);
};
const handleDisplayNameSave = () => {
if (displayName !== userData.displayName) {
updateUserDetails({ displayName: displayName ?? '' });
}
setIsDisplayNameEdit(false);
};
const displayNameRenderComponent = useMemo(
() =>
isDisplayNameEdit && hasEditPermission ? (
<InlineEdit
direction="vertical"
onCancel={() => setIsDisplayNameEdit(false)}
onSave={handleDisplayNameSave}>
<Input
className="w-full"
data-testid="displayName"
id="displayName"
name="displayName"
placeholder={t('label.display-name')}
type="text"
value={displayName}
onChange={onDisplayNameChange}
/>
</InlineEdit>
) : (
<Row align="middle" wrap={false}>
<Col flex="auto">
<Typography.Text
className="text-lg font-medium"
ellipsis={{ tooltip: true }}>
{hasEditPermission
? userData.displayName ??
t('label.add-entity', { entity: t('label.display-name') })
: getEntityName(userData)}
</Typography.Text>
</Col>
<Col className="d-flex justify-end" flex="25px">
{hasEditPermission && (
<EditIcon
className="cursor-pointer"
color={DE_ACTIVE_COLOR}
data-testid="edit-displayName"
{...ICON_DIMENSION}
onClick={() => setIsDisplayNameEdit(true)}
/>
)}
</Col>
</Row>
),
[
userData,
isDisplayNameEdit,
hasEditPermission,
getEntityName,
onDisplayNameChange,
handleDisplayNameSave,
]
);
const descriptionRenderComponent = useMemo(
() =>
hasEditPermission ? (
<DescriptionV1
reduceDescription
description={userData.description ?? ''}
entityName={getEntityName(userData as unknown as EntityReference)}
entityType={EntityType.USER}
hasEditAccess={isAdminUser}
isEdit={isDescriptionEdit}
showCommentsIcon={false}
onCancel={() => setIsDescriptionEdit(false)}
onDescriptionEdit={() => setIsDescriptionEdit(true)}
onDescriptionUpdate={handleDescriptionChange}
/>
) : (
<Typography.Paragraph className="m-b-0">
{userData.description ?? (
<span className="text-grey-muted">
{t('label.no-entity', {
entity: t('label.description'),
})}
</span>
)}
</Typography.Paragraph>
),
[
userData,
isAdminUser,
isDescriptionEdit,
hasEditPermission,
getEntityName,
handleDescriptionChange,
]
);
const changePasswordRenderComponent = useMemo(
() =>
isAuthProviderBasic &&
(isAdminUser || isLoggedInUser) && (
<Button
className="w-full text-xs"
type="primary"
onClick={() => setIsChangePassword(true)}>
{t('label.change-entity', {
entity: t('label.password-lowercase'),
})}
</Button>
),
[isAuthProviderBasic, isAdminUser, isLoggedInUser]
);
const handleChangePassword = async (data: ChangePasswordRequest) => {
try {
setIsLoading(true);
const sendData = {
...data,
...(isAdminUser &&
!isLoggedInUser && {
username: userData.name,
requestType: RequestType.User,
}),
};
await changePassword(sendData);
setIsChangePassword(false);
showSuccessToast(
t('server.update-entity-success', { entity: t('label.password') })
);
} catch (err) {
showErrorToast(err as AxiosError);
} finally {
setIsLoading(true);
}
};
return (
<Space
className="p-sm w-full"
data-testid="user-profile-details"
direction="vertical"
size="middle">
<Space className="w-full" direction="vertical" size={2}>
{displayNameRenderComponent}
<Typography.Paragraph
className="m-b-0 text-grey-muted"
ellipsis={{ tooltip: true }}>
{userData.email}
</Typography.Paragraph>
</Space>
<Space direction="vertical" size="middle">
{descriptionRenderComponent}
{changePasswordRenderComponent}
</Space>
<ChangePasswordForm
isLoading={isLoading}
isLoggedInUser={isLoggedInUser}
visible={isChangePassword}
onCancel={() => setIsChangePassword(false)}
onSave={(data) => handleChangePassword(data)}
/>
</Space>
);
};
export default UserProfileDetails;

View File

@ -0,0 +1,24 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { User } from 'generated/entity/teams/user';
export interface UserProfileDetailsProps {
userData: {
email: string;
name: string;
displayName?: string;
description?: string;
};
updateUserDetails: (data: Partial<User>) => Promise<void>;
}

View File

@ -0,0 +1,64 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Image } from 'antd';
import ProfilePicture from 'components/common/ProfilePicture/ProfilePicture';
import React, { useEffect, useMemo, useState } from 'react';
import {
getImageWithResolutionAndFallback,
ImageQuality,
} from 'utils/ProfilerUtils';
import { UserProfileImageProps } from './UserProfileImage.interface';
const UserProfileImage = ({ userData }: UserProfileImageProps) => {
const [isImgUrlValid, SetIsImgUrlValid] = useState<boolean>(true);
const image = useMemo(
() =>
getImageWithResolutionAndFallback(ImageQuality['6x'], userData?.images),
[userData?.images]
);
useEffect(() => {
if (image) {
SetIsImgUrlValid(true);
}
}, [image]);
return (
<div className="profile-image-container">
{isImgUrlValid ? (
<Image
alt="profile"
preview={false}
referrerPolicy="no-referrer"
src={image ?? ''}
onError={() => {
SetIsImgUrlValid(false);
}}
/>
) : (
<ProfilePicture
displayName={userData?.displayName ?? userData.name}
height="186"
id={userData?.id ?? ''}
name={userData?.name ?? ''}
textClass="text-5xl"
width=""
/>
)}
</div>
);
};
export default UserProfileImage;

View File

@ -0,0 +1,23 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ImageList } from 'generated/type/profile';
export interface UserProfileImageProps {
userData: {
id?: string;
name?: string;
displayName?: string;
images?: ImageList;
};
}

View File

@ -0,0 +1,47 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Card, Typography } from 'antd';
import Chip from 'components/common/Chip/Chip.component';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as UserIcons } from '../../../../assets/svg/user.svg';
import { UserProfileInheritedRolesProps } from './UserProfileInheritedRoles.interface';
const UserProfileInheritedRoles = ({
inheritedRoles,
}: UserProfileInheritedRolesProps) => {
const { t } = useTranslation();
return (
<Card
className="ant-card-feed relative card-body-border-none card-padding-y-0"
data-testid="user-profile-inherited-roles"
key="inherited-roles-card-component"
title={
<Typography.Text
className="right-panel-label m-b-0"
data-testid="inherited-roles">
{t('label.inherited-role-plural')}
</Typography.Text>
}>
<Chip
data={inheritedRoles ?? []}
icon={<UserIcons className="cursor-pointer" height={20} width={20} />}
noDataPlaceholder={t('message.no-inherited-roles-found')}
/>
</Card>
);
};
export default UserProfileInheritedRoles;

View File

@ -0,0 +1,18 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { User } from 'generated/entity/teams/user';
export interface UserProfileInheritedRolesProps {
inheritedRoles: User['inheritedRoles'];
}

View File

@ -0,0 +1,193 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Card, Select, Space, Typography } from 'antd';
import { AxiosError } from 'axios';
import Chip from 'components/common/Chip/Chip.component';
import InlineEdit from 'components/InlineEdit/InlineEdit.component';
import {
DE_ACTIVE_COLOR,
ICON_DIMENSION,
PAGE_SIZE_LARGE,
TERM_ADMIN,
} from 'constants/constants';
import { Role } from 'generated/entity/teams/role';
import { useAuth } from 'hooks/authHooks';
import { isEmpty, toLower } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { getRoles } from 'rest/rolesAPIV1';
import { getEntityName } from 'utils/EntityUtils';
import { showErrorToast } from 'utils/ToastUtils';
import { ReactComponent as EditIcon } from '../../../../assets/svg/edit-new.svg';
import { ReactComponent as UserIcons } from '../../../../assets/svg/user.svg';
import { UserProfileRolesProps } from './UserProfileRoles.interface';
const UserProfileRoles = ({
isUserAdmin,
userRoles,
updateUserDetails,
}: UserProfileRolesProps) => {
const { t } = useTranslation();
const { isAdminUser } = useAuth();
const [isRolesEdit, setIsRolesEdit] = useState(false);
const [isRolesLoading, setIsRolesLoading] = useState(false);
const [selectedRoles, setSelectedRoles] = useState<string[]>([]);
const [roles, setRoles] = useState<Role[]>([]);
const useRolesOption = useMemo(() => {
const options = roles?.map((role) => ({
label: getEntityName(role),
value: role.id,
}));
if (!isUserAdmin) {
options.push({
label: TERM_ADMIN,
value: toLower(TERM_ADMIN),
});
}
return options;
}, [roles]);
const fetchRoles = async () => {
setIsRolesLoading(true);
try {
const response = await getRoles(
'',
undefined,
undefined,
false,
PAGE_SIZE_LARGE
);
setRoles(response.data);
} catch (err) {
showErrorToast(
err as AxiosError,
t('server.entity-fetch-error', {
entity: t('label.role-plural'),
})
);
} finally {
setIsRolesLoading(false);
}
};
const handleRolesSave = () => {
// filter out the roles , and exclude the admin one
const updatedRoles = selectedRoles.filter(
(roleId) => roleId !== toLower(TERM_ADMIN)
);
// get the admin role and send it as boolean value `isAdmin=Boolean(isAdmin)
const isAdmin = selectedRoles.find(
(roleId) => roleId === toLower(TERM_ADMIN)
);
updateUserDetails({
roles: updatedRoles.map((roleId) => {
const role = roles.find((r) => r.id === roleId);
return { id: roleId, type: 'role', name: role?.name ?? '' };
}),
isAdmin: Boolean(isAdmin),
});
setIsRolesEdit(false);
};
const rolesRenderElement = useMemo(
() => (
<Chip
data={[
...(isUserAdmin
? [{ id: 'admin', type: 'role', name: TERM_ADMIN }]
: []),
...(userRoles ?? []),
]}
icon={<UserIcons className="cursor-pointer" height={20} width={20} />}
noDataPlaceholder={t('message.no-roles-assigned')}
showNoDataPlaceholder={!isUserAdmin}
/>
),
[userRoles, isUserAdmin]
);
useEffect(() => {
const defaultUserRoles = [
...(userRoles?.map((role) => role.id) ?? []),
...(isUserAdmin ? [toLower(TERM_ADMIN)] : []),
];
setSelectedRoles(defaultUserRoles);
}, [isUserAdmin, userRoles]);
useEffect(() => {
if (isRolesEdit && isEmpty(roles)) {
fetchRoles();
}
}, [isRolesEdit, roles]);
return (
<Card
className="ant-card-feed relative card-body-border-none card-padding-y-0"
data-testid="user-profile-roles"
key="roles-card"
title={
<Space align="center">
<Typography.Text className="right-panel-label">
{t('label.role-plural')}
</Typography.Text>
{!isRolesEdit && isAdminUser && (
<EditIcon
className="cursor-pointer"
color={DE_ACTIVE_COLOR}
data-testid="edit-roles"
{...ICON_DIMENSION}
onClick={() => setIsRolesEdit(true)}
/>
)}
</Space>
}>
<div className="m-b-md">
{isRolesEdit && isAdminUser ? (
<InlineEdit
direction="vertical"
onCancel={() => setIsRolesEdit(false)}
onSave={handleRolesSave}>
<Select
allowClear
showSearch
aria-label="Select roles"
className="w-full"
id="select-role"
loading={isRolesLoading}
maxTagCount={4}
mode="multiple"
options={useRolesOption}
placeholder={t('label.role-plural')}
value={!isRolesLoading ? selectedRoles : []}
onChange={setSelectedRoles}
/>
</InlineEdit>
) : (
rolesRenderElement
)}
</div>
</Card>
);
};
export default UserProfileRoles;

View File

@ -0,0 +1,19 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { User } from 'generated/entity/teams/user';
export interface UserProfileRolesProps {
isUserAdmin?: boolean;
userRoles: User['roles'];
updateUserDetails: (data: Partial<User>) => Promise<void>;
}

View File

@ -0,0 +1,102 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Card, Space, Typography } from 'antd';
import Chip from 'components/common/Chip/Chip.component';
import InlineEdit from 'components/InlineEdit/InlineEdit.component';
import TeamsSelectable from 'components/TeamsSelectable/TeamsSelectable';
import { DE_ACTIVE_COLOR, ICON_DIMENSION } from 'constants/constants';
import { useAuth } from 'hooks/authHooks';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { getNonDeletedTeams } from 'utils/CommonUtils';
import { ReactComponent as EditIcon } from '../../../../assets/svg/edit-new.svg';
import { ReactComponent as IconTeamsGrey } from '../../../../assets/svg/teams-grey.svg';
import { UserProfileTeamsProps } from './UserProfileTeams.interface';
const UserProfileTeams = ({
teams,
updateUserDetails,
}: UserProfileTeamsProps) => {
const { t } = useTranslation();
const { isAdminUser } = useAuth();
const [isTeamsEdit, setIsTeamsEdit] = useState(false);
const [selectedTeams, setSelectedTeams] = useState<string[]>([]);
const handleTeamsSave = () => {
updateUserDetails({
teams: selectedTeams.map((teamId) => ({ id: teamId, type: 'team' })),
});
setIsTeamsEdit(false);
};
const teamsRenderElement = useMemo(
() => (
<Chip
data={getNonDeletedTeams(teams ?? [])}
icon={<IconTeamsGrey height={20} width={20} />}
noDataPlaceholder={t('message.no-team-found')}
/>
),
[teams, getNonDeletedTeams]
);
useEffect(() => {
setSelectedTeams(getNonDeletedTeams(teams ?? []).map((team) => team.id));
}, [teams]);
return (
<Card
className="relative card-body-border-none card-padding-y-0"
key="teams-card"
title={
<Space align="center">
<Typography.Text className="right-panel-label">
{t('label.team-plural')}
</Typography.Text>
{!isTeamsEdit && isAdminUser && (
<EditIcon
className="cursor-pointer"
color={DE_ACTIVE_COLOR}
data-testid="edit-teams"
{...ICON_DIMENSION}
onClick={() => setIsTeamsEdit(true)}
/>
)}
</Space>
}>
<div className="m-b-md">
{isTeamsEdit && isAdminUser ? (
<InlineEdit
direction="vertical"
onCancel={() => setIsTeamsEdit(false)}
onSave={handleTeamsSave}>
<TeamsSelectable
filterJoinable
maxValueCount={4}
selectedTeams={selectedTeams}
onSelectionChange={setSelectedTeams}
/>
</InlineEdit>
) : (
teamsRenderElement
)}
</div>
</Card>
);
};
export default UserProfileTeams;

View File

@ -0,0 +1,18 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { User } from 'generated/entity/teams/user';
export interface UserProfileTeamsProps {
teams: User['teams'];
updateUserDetails: (data: Partial<User>) => Promise<void>;
}

View File

@ -0,0 +1,82 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Popover, Space, Tag, Typography } from 'antd';
import { USER_DATA_SIZE } from 'constants/constants';
import { EntityReference } from 'generated/entity/type';
import { isEmpty } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import { getEntityName } from 'utils/EntityUtils';
import { ChipProps } from './Chip.interface';
const Chip = ({
data,
icon,
noDataPlaceholder,
showNoDataPlaceholder = true,
}: ChipProps) => {
const [listLength, setListLength] = useState<number>(0);
const hasMoreElement = useMemo(
() => listLength > USER_DATA_SIZE,
[listLength]
);
const getChipElement = (item: EntityReference) => (
<Space
align="center"
className="w-full m-b-xs"
data-testid={item.name}
key={item.name}
size={6}>
{icon}
<Typography.Text className="w-56 text-left" ellipsis={{ tooltip: true }}>
{getEntityName(item)}
</Typography.Text>
</Space>
);
useEffect(() => {
setListLength(data?.length ?? 0);
}, [data]);
if (isEmpty(data) && showNoDataPlaceholder) {
return (
<Typography.Paragraph className="text-grey-muted">
{noDataPlaceholder}
</Typography.Paragraph>
);
}
return (
<Space wrap data-testid="chip-container" size={4}>
{data.slice(0, USER_DATA_SIZE).map(getChipElement)}
{hasMoreElement && (
<Popover
className="cursor-pointer"
content={
<Space wrap size={6}>
{data.slice(USER_DATA_SIZE).map(getChipElement)}
</Space>
}
overlayClassName="w-56"
trigger="click">
<Tag className="m-l-xss" data-testid="plus-more-count">{`+${
listLength - USER_DATA_SIZE
} more`}</Tag>
</Popover>
)}
</Space>
);
};
export default Chip;

View File

@ -0,0 +1,20 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EntityReference } from 'generated/entity/type';
export interface ChipProps {
data: EntityReference[];
icon: React.ReactElement;
noDataPlaceholder: string;
showNoDataPlaceholder?: boolean;
}

View File

@ -34,4 +34,5 @@ export interface DescriptionProps {
onCancel?: () => void;
onDescriptionUpdate?: (value: string) => Promise<void>;
onSuggest?: (value: string) => void;
reduceDescription?: boolean;
}

View File

@ -13,7 +13,7 @@
import { Button, Popover, Space, Typography } from 'antd';
import { ReactComponent as IconEdit } from 'assets/svg/edit-new.svg';
import { AxiosError } from 'axios';
import { DE_ACTIVE_COLOR } from 'constants/constants';
import { DE_ACTIVE_COLOR, ICON_DIMENSION } from 'constants/constants';
import { t } from 'i18next';
import { isFunction, isUndefined } from 'lodash';
import React, { FC, Fragment } from 'react';
@ -21,7 +21,7 @@ import { useHistory } from 'react-router-dom';
import { ReactComponent as IconCommentPlus } from '../../../assets/svg/add-chat.svg';
import { ReactComponent as IconComments } from '../../../assets/svg/comment.svg';
import { ReactComponent as IconRequest } from '../../../assets/svg/request-icon.svg';
import { ReactComponent as IconTaskColor } from '../../../assets/svg/Task-ic.svg';
import { ReactComponent as IconTaskColor } from '../../../assets/svg/task-ic.svg';
import { EntityField } from '../../../constants/Feeds.constants';
import { EntityType } from '../../../enums/entity.enum';
import { ThreadType } from '../../../generated/entity/feed/thread';
@ -55,6 +55,7 @@ const Description: FC<DescriptionProps> = ({
entityType,
entityFqn,
entityFieldTasks,
reduceDescription,
}) => {
const history = useHistory();
@ -111,9 +112,8 @@ const Description: FC<DescriptionProps> = ({
trigger="hover"
zIndex={9999}>
<IconRequest
height={16}
name={t('message.request-description')}
width={16}
{...ICON_DIMENSION}
/>
</Popover>
</Button>
@ -132,7 +132,7 @@ const Description: FC<DescriptionProps> = ({
type="text"
onClick={() => onThreadLinkSelect?.(descriptionThread.entityLink)}>
<Space align="center" className="h-full" size={2}>
<IconComments height={16} name="tasks" width={16} />
<IconComments {...ICON_DIMENSION} name="tasks" />
<Typography.Text data-testid="description-thread-count">
{descriptionThread.count}
</Typography.Text>
@ -144,7 +144,7 @@ const Description: FC<DescriptionProps> = ({
<Button
className="w-7 h-7 link-text p-0 flex-center"
data-testid="start-description-thread"
icon={<IconCommentPlus height={16} name="comments" width={16} />}
icon={<IconCommentPlus {...ICON_DIMENSION} name="comments" />}
type="text"
onClick={() =>
onThreadLinkSelect?.(
@ -168,7 +168,7 @@ const Description: FC<DescriptionProps> = ({
type="text"
onClick={() => onThreadLinkSelect?.(tasks.entityLink, ThreadType.Task)}>
<Space align="center" className="h-full" size={2}>
<IconTaskColor height={16} name="tasks" width={16} />
<IconTaskColor {...ICON_DIMENSION} name="tasks" />
<Typography.Text data-testid="description-tasks-count">
{tasks.count}
</Typography.Text>
@ -183,7 +183,7 @@ const Description: FC<DescriptionProps> = ({
<Button
className="w-7 h-7 p-0 flex-center"
data-testid="edit-description"
icon={<IconEdit color={DE_ACTIVE_COLOR} height={16} width={16} />}
icon={<IconEdit color={DE_ACTIVE_COLOR} {...ICON_DIMENSION} />}
type="text"
onClick={handleUpdate}
/>
@ -210,6 +210,7 @@ const Description: FC<DescriptionProps> = ({
id="center">
{description?.trim() ? (
<RichTextEditorPreviewer
className={reduceDescription ? 'max-two-lines' : ''}
enableSeeMoreVariant={!removeBlur}
markdown={description}
/>

View File

@ -55,6 +55,7 @@ interface Props {
wrapInCard?: boolean;
isVersionView?: boolean;
showCommentsIcon?: boolean;
reduceDescription?: boolean;
}
const DescriptionV1 = ({
hasEditAccess,
@ -73,6 +74,7 @@ const DescriptionV1 = ({
wrapInCard = false,
isVersionView,
showCommentsIcon = true,
reduceDescription,
}: Props) => {
const descriptionThread = entityFieldThreads?.[0];
const history = useHistory();
@ -176,6 +178,7 @@ const DescriptionV1 = ({
<div>
{description?.trim() ? (
<RichTextEditorPreviewer
className={reduceDescription ? 'max-two-lines' : ''}
enableSeeMoreVariant={!removeBlur}
markdown={description}
/>

View File

@ -48,6 +48,7 @@ export const SUPPORTED_FIELD_TYPES = ['string', 'markdown', 'integer'];
export const LOGGED_IN_USER_STORAGE_KEY = 'loggedInUsers';
export const TAG_VIEW_CAP = 33;
export const USER_DATA_SIZE = 4;
export const FOLLOWERS_VIEW_CAP = 20;
export const INITIAL_PAGING_VALUE = 1;
export const JSON_TAB_SIZE = 2;
@ -803,3 +804,8 @@ export const VALIDATION_MESSAGES = {
export const ERROR_MESSAGE = {
alreadyExist: 'already exists',
};
export const ICON_DIMENSION = {
with: 14,
height: 14,
};

View File

@ -966,6 +966,7 @@
"try-again": "Try Again",
"tuesday": "Tuesday",
"type": "Type",
"type-entities": "{{type}} Entities",
"type-filed-name": "Type {{fieldName}}",
"type-lowercase": "type",
"type-to-confirm": "Type <0>{{text}}</0> to confirm",
@ -1286,7 +1287,7 @@
"no-terms-found-for-search-text": "No terms found for {{searchText}}",
"no-token-available": "No token available",
"no-user-available": "No user available",
"no-username-available": "No user available with name",
"no-username-available": "No user available with name <0>{{user}}</0>",
"no-users": "There are no users {{text}}",
"no-version-type-available": "No {{type}} version available",
"nodes-per-layer-message": "Please enter a value for nodes per layer",

View File

@ -966,6 +966,7 @@
"try-again": "Try Again",
"tuesday": "Martes",
"type": "Tipo",
"type-entities": "{{type}} Entities",
"type-filed-name": "Tipo {{fieldName}}",
"type-lowercase": "tipo",
"type-to-confirm": "Escriba <0>{{text}}</0> para confirmar",
@ -1286,7 +1287,7 @@
"no-terms-found-for-search-text": "No se encontraron términos para {{searchText}}",
"no-token-available": "No hay tokens disponibles",
"no-user-available": "No hay usuarios disponibles",
"no-username-available": "No hay usuarios disponibles con el nombre",
"no-username-available": "No hay usuarios disponibles con el nombre <0>{{user}}</0>",
"no-users": "No hay usuarios {{text}}",
"no-version-type-available": "No hay versiones de {{type}} disponible",
"nodes-per-layer-message": "Por favor, ingrese un valor para nodos por capa",

View File

@ -966,6 +966,7 @@
"try-again": "Try Again",
"tuesday": "Mardi",
"type": "Type",
"type-entities": "{{type}} Entities",
"type-filed-name": "Type {{fieldName}}",
"type-lowercase": "type",
"type-to-confirm": "Entrer <0>{{text}}</0> pour confirmer",
@ -1286,7 +1287,7 @@
"no-terms-found-for-search-text": "Aucun terme trouvé pour {{searchText}}",
"no-token-available": "Aucun jeton disponible",
"no-user-available": "Aucun utilisateur disponible",
"no-username-available": "Aucun utilisateur disponible avec le nom",
"no-username-available": "Aucun utilisateur disponible avec le nom <0>{{user}}</0>",
"no-users": "Il n'y a pas d'utilisteurs {{text}}",
"no-version-type-available": "Aucune version {{type}} disponible",
"nodes-per-layer-message": "Merci de saisir une valeur pour le nombre de noeuds par couche",

View File

@ -966,6 +966,7 @@
"try-again": "Try Again",
"tuesday": "火曜日",
"type": "Type",
"type-entities": "{{type}} Entities",
"type-filed-name": "Type {{fieldName}}",
"type-lowercase": "type",
"type-to-confirm": "Type <0>{{text}}</0> to confirm",
@ -1286,7 +1287,7 @@
"no-terms-found-for-search-text": "No terms found for {{searchText}}",
"no-token-available": "No token available",
"no-user-available": "No user available",
"no-username-available": "No user available with name",
"no-username-available": "No user available with name <0>{{user}}</0>",
"no-users": "ユーザ{{text}}は存在しません",
"no-version-type-available": "No {{type}} version available",
"nodes-per-layer-message": "Please enter a value for nodes per layer",

View File

@ -966,6 +966,7 @@
"try-again": "Try Again",
"tuesday": "Terça-feira",
"type": "Tipo",
"type-entities": "{{type}} Entities",
"type-filed-name": "Digite {{fieldName}}",
"type-lowercase": "digite",
"type-to-confirm": "Digite <0>{{text}}</0> para confirmar",
@ -1286,7 +1287,7 @@
"no-terms-found-for-search-text": "Nenhum termo encontrado para {{searchText}}",
"no-token-available": "Nenhum token disponível",
"no-user-available": "Nenhum usuário disponível",
"no-username-available": "Nenhum usuário disponível com este nome",
"no-username-available": "Nenhum usuário disponível com este nome <0>{{user}}</0>",
"no-users": "Não há usuários {{text}}",
"no-version-type-available": "Nenhuma versão de {{type}} disponível",
"nodes-per-layer-message": "Por favor, insira um valor para o número de nós por camada",

View File

@ -966,6 +966,7 @@
"try-again": "Try Again",
"tuesday": "星期二",
"type": "类型",
"type-entities": "{{type}} Entities",
"type-filed-name": "{{fieldName}}类型",
"type-lowercase": "类型",
"type-to-confirm": "输入<0>{{text}}</0>进行确认",
@ -1286,7 +1287,7 @@
"no-terms-found-for-search-text": "未找到{{searchText}}的术语",
"no-token-available": "无可用令牌",
"no-user-available": "无可用用户",
"no-username-available": "没有用户名为",
"no-username-available": "没有用户名为 <0>{{user}}</0>",
"no-users": "没有用户{{text}}",
"no-version-type-available": "无{{type}}版本可用",
"nodes-per-layer-message": "请输入每层节点的值",

View File

@ -11,15 +11,16 @@
* limitations under the License.
*/
import { Typography } from 'antd';
import { AxiosError } from 'axios';
import { useAuthContext } from 'components/authentication/auth-provider/AuthProvider';
import Loader from 'components/Loader/Loader';
import Users from 'components/Users/Users.component';
import { compare } from 'fast-json-patch';
import { isEmpty } from 'lodash';
import { observer } from 'mobx-react';
import Qs from 'qs';
import React, {
import {
default as React,
Dispatch,
SetStateAction,
useEffect,
@ -30,12 +31,11 @@ import { useTranslation } from 'react-i18next';
import { useHistory, useParams } from 'react-router-dom';
import { searchData } from 'rest/miscAPI';
import { getUserByName, updateUserDetail } from 'rest/userAPI';
import AppState from '../../AppState';
import { Transi18next } from 'utils/CommonUtils';
import { PAGE_SIZE } from '../../constants/constants';
import { myDataSearchIndex } from '../../constants/Mydata.constants';
import { UserProfileTab } from '../../enums/user.enum';
import { User } from '../../generated/entity/teams/user';
import { useAuth } from '../../hooks/authHooks';
import { SearchEntityHits } from '../../utils/APIUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import { UserAssetsDataType } from './UserPage.interface';
@ -45,11 +45,8 @@ const UserPage = () => {
const { t } = useTranslation();
const { username, tab = UserProfileTab.ACTIVITY } =
useParams<{ [key: string]: string }>();
const { isAdminUser } = useAuth();
const { isAuthDisabled } = useAuthContext();
const [isLoading, setIsLoading] = useState(true);
const [userData, setUserData] = useState<User>({} as User);
const [currentLoggedInUser, setCurrentLoggedInUser] = useState<User>();
const [isError, setIsError] = useState(false);
const [isUserEntitiesLoading, setIsUserEntitiesLoading] =
useState<boolean>(false);
@ -74,26 +71,23 @@ const UserPage = () => {
[location.search]
);
const fetchUserData = () => {
setUserData({} as User);
getUserByName(username, 'profile,roles,teams')
.then((res) => {
if (res) {
setUserData(res);
} else {
throw t('server.unexpected-response');
}
})
.catch((err: AxiosError) => {
showErrorToast(
err,
t('server.entity-fetch-error', {
entity: 'User Details',
})
);
setIsError(true);
})
.finally(() => setIsLoading(false));
const fetchUserData = async () => {
try {
const res = await getUserByName(username, 'profile,roles,teams');
setUserData(res);
} catch (error) {
showErrorToast(
error as AxiosError,
t('server.entity-fetch-error', {
entity: t('label.entity-detail-plural', {
entity: t('label.user'),
}),
})
);
setIsError(true);
} finally {
setIsLoading(false);
}
};
const getQueryFilters = (fetchOwnedEntities: boolean) => {
@ -145,7 +139,11 @@ const UserPage = () => {
showErrorToast(
error as AxiosError,
t('server.entity-fetch-error', {
entity: `${fetchOwnedEntities ? 'Owned' : 'Follwing'} Entities`,
entity: t('label.type-entities', {
type: fetchOwnedEntities
? t('label.owner')
: t('label.following'),
}),
})
);
} finally {
@ -165,12 +163,17 @@ const UserPage = () => {
<div
className="d-flex flex-col tw-items-center tw-place-content-center tw-mt-40 tw-gap-1"
data-testid="error">
<p className="tw-text-base" data-testid="error-message">
{t('message.no-username-available')}
<span className="tw-font-medium" data-testid="username">
{username}
</span>
</p>
<Typography.Paragraph
className="tw-text-base"
data-testid="error-message">
<Transi18next
i18nKey="message.no-username-available"
renderElement={<strong data-testid="username" />}
values={{
user: username,
}}
/>
</Typography.Paragraph>
</div>
);
};
@ -191,31 +194,6 @@ const UserPage = () => {
}
};
const isLoggedinUser = (userName: string) => {
return userName === currentLoggedInUser?.name;
};
const getUserComponent = () => {
if (!isError && !isEmpty(userData)) {
return (
<Users
followingEntities={followingEntities}
handlePaginate={handleEntityPaginate}
isAdminUser={Boolean(isAdminUser)}
isAuthDisabled={Boolean(isAuthDisabled)}
isLoggedinUser={isLoggedinUser(username)}
isUserEntitiesLoading={isUserEntitiesLoading}
ownedEntities={ownedEntities}
updateUserDetails={updateUserDetails}
userData={userData}
username={username}
/>
);
} else {
return <ErrorPlaceholder />;
}
};
useEffect(() => {
fetchUserData();
}, [username]);
@ -232,11 +210,25 @@ const UserPage = () => {
}
}, [page, tab, userData]);
useEffect(() => {
setCurrentLoggedInUser(AppState.getCurrentUserDetails());
}, [AppState.nonSecureUserDetails, AppState.userDetails]);
if (isLoading) {
return <Loader />;
}
return <>{isLoading ? <Loader /> : getUserComponent()}</>;
if (isError && isEmpty(userData)) {
return <ErrorPlaceholder />;
}
return (
<Users
followingEntities={followingEntities}
handlePaginate={handleEntityPaginate}
isUserEntitiesLoading={isUserEntitiesLoading}
ownedEntities={ownedEntities}
updateUserDetails={updateUserDetails}
userData={userData}
username={username}
/>
);
};
export default observer(UserPage);

View File

@ -172,6 +172,15 @@ a[href].link-text-grey,
white-space: pre-wrap;
}
// Reduce text to 2 lines
.max-two-lines {
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}
.mx-auto {
margin-right: auto;
margin-left: auto;

View File

@ -44,6 +44,12 @@
.w-40 {
width: 10rem /* 160px */;
}
.w-48 {
width: 12rem;
}
.w-56 {
width: 14rem;
}
.w-68 {
width: 17rem;
}
@ -53,9 +59,6 @@
.w-72 {
width: 288px;
}
.w-48 {
width: 12rem;
}
.w-500 {
width: 500px;
}

View File

@ -70,6 +70,11 @@ pre {
line-height: 2rem /* 32px */;
}
.text-5xl {
font-size: 3rem /* 48px */;
line-height: 3rem /* 48px */;
}
// font color
.text-base-color {
color: @text-color;

View File

@ -77,6 +77,7 @@
@tag-default-bg: @grey-1;
@grey-bg-with-alpha: #7575751a;
@btn-shadow: none;
@user-profile-background: rgba(9, 80, 197, 0.05);
@navbar-height: 64px;
@sidebar-width: 60px;
@ -86,7 +87,7 @@
@left-side-panel-width: 230px;
@entity-details-tab-height: calc(100vh - 236px);
@users-page-tabs-height: calc(
100vh - 122px
100vh - 120px
); /* navbar+tab_height+padding = 64+46+12 */
@glossary-page-height: calc(100vh - 165px);
@glossary-term-page-height: calc(100vh - 200px);

View File

@ -185,7 +185,7 @@ import IconTableGrey from '../assets/svg/table-grey.svg';
import IconTable from '../assets/svg/table.svg';
import IconTagGrey from '../assets/svg/tag-grey.svg';
import IconTag from '../assets/svg/tag.svg';
import IconTaskColor from '../assets/svg/Task-ic.svg';
import IconTaskColor from '../assets/svg/task-ic.svg';
import IconTeamsGrey from '../assets/svg/teams-grey.svg';
import IconTerns from '../assets/svg/terms.svg';
import IconTier from '../assets/svg/tier.svg';